/*
now.js javascript client has been modified and adapted for titanium mobile
by jordi domenech <jordi@iamyellow.net>
https://github.com/iamyellow
*/

/*
Copyright (C) 2011 by Flotype Inc. <team@nowjs.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

var io = require('socket.io');

(function () {
  var nowObjects = {};
  var noConflict = function (uri, options) {
    uri = uri || '';
    if (nowObjects[uri]) {
      return nowObjects[uri];
    }
    options = options || {};
    options.socketio = options.socketio || {};
    options.socketio.resource = options.socketio.resource || "socket.io";

    var socket;
    var closures = {};
    var nowReady = false;
    var readied = 0;
    var lastTimeout;

    var util, lib;

    var isIE = false;

    var fqnMap = {
      data: {},
      arrays: {},
      get: function (fqn) {
        return fqnMap.data[fqn];
      },
      set: function (fqn, val) {
        if (fqnMap.data[fqn] !== undefined) {
          fqnMap.deleteChildren(fqn, val);
        } else {
          var lastIndex = fqn.lastIndexOf('.');
          var parent = fqn.substring(0, lastIndex);
          fqnMap.addParent(parent, fqn.substring(lastIndex + 1));
        }
        return (fqnMap.data[fqn] = val);
      },
      addParent: function (parent, key) {
        if (parent) {
          if (!util.isArray(fqnMap.data[parent])) {
            fqnMap.set(parent, []); // Handle changing a non-object to an object.
          }
          fqnMap.data[parent].push(key);
        }
      },
      deleteChildren: function (fqn) {
        var keys = this.data[fqn];
        var children = [];
        if (util.isArray(this.data[fqn])) {
          // Deleting a child will remove it via splice.
          for (var i = 0; keys.length;) {
            // Recursive delete all children.
            var arr = this.deleteVar(fqn + '.' + keys[i]);
            for (var j = 0; j < arr.length; j++) {
              children.push(arr[j]);
            }
          }
        }
        return children;
      },
      deleteVar: function (fqn) {
        var lastIndex = fqn.lastIndexOf('.');
        var parent = fqn.substring(0, lastIndex);
        if (util.hasProperty(this.data, parent)) {
          var index = util.indexOf(this.data[parent], fqn.substring(lastIndex + 1));
          if (index > -1) {
            this.data[parent].splice(index, 1);
          }
        }
        var children = this.deleteChildren(fqn);
        children.push(fqn);
        delete this.data[fqn];
        this.unflagAsArray(fqn);
        return children;
      },
      flagAsArray: function (val) {
        return (this.arrays[val] = true);
      },
      unflagAsArray: function (val) {
        delete this.arrays[val];
      }
    };
    util = {
      _events: {},
      // Event code from socket.io
      on: function (name, fn) {
        if (!(util.hasProperty(util._events, name))) {
          util._events[name] = [];
        }
        util._events[name].push(fn);
        return util;
      },

      indexOf: function (arr, val) {
        for (var i = 0, ii = arr.length; i < ii; i++) {
          if ("" + arr[i] === val) {
            return i;
          }
        }
        return -1;
      },

      emit: function (name, args) {
        if (util.hasProperty(util._events, name)) {
          var events = util._events[name].slice(0);
          for (var i = 0, ii = events.length; i < ii; i++) {
            events[i].apply(util, args === undefined ? [] : args);
          }
        }
        return util;
      },
      removeEvent: function (name, fn) {
        if (util.hasProperty(util._events, name)) {
          for (var a = 0, l = util._events[name].length; a < l; a++) {
            if (util._events[name][a] === fn) {
              util._events[name].splice(a, 1);
            }
          }
        }
        return util;
      },

      hasProperty: function (obj, prop) {
        return Object.prototype.hasOwnProperty.call(Object(obj), prop);
      },
      isArray: Array.isArray || function (obj) {
        return Object.prototype.toString.call(obj) === '[object Array]';
      },

      createVarAtFqn: function (scope, fqn, value) {
        var path = fqn.split('.');
        var currVar = util.forceGetParentVarAtFqn(scope, fqn);
        var key = path.pop();
        fqnMap.set(fqn, (value && typeof value === 'object') ? [] : value);
        if (util.isArray(value)) {
          fqnMap.flagAsArray(fqn);
        }
        currVar[key] = value;
        if (!(isIE || util.isArray(currVar))) {
          util.watch(currVar, key, fqn);
        }
      },

      forceGetParentVarAtFqn: function (scope, fqn) {
        var path = fqn.split('.');
        path.shift();
        var currVar = scope;
        while (path.length > 1) {
          var prop = path.shift();
          if (!util.hasProperty(currVar, prop)) {
            if (!isNaN(path[0])) {
              currVar[prop] = [];
            } else {
              currVar[prop] = {};
            }
          }
          if(!(currVar[prop] && typeof currVar[prop] === "object")) {
            currVar[prop] = {};
          } 
          currVar = currVar[prop];
        }
        return currVar;
      },

      getVarFromFqn: function (scope, fqn) {
        var path = fqn.split('.');
        path.shift();
        var currVar = scope;
        while (path.length > 0) {
          var prop = path.shift();
          if (util.hasProperty(currVar, prop)) {
            currVar = currVar[prop];
          } else {
            return false;
          }
        }
        return currVar;
      },

      generateRandomString: function () {
        return Math.random().toString().substr(2);
      },

      getValOrFqn: function (val, fqn) {
        if (typeof val === 'function') {
          if (val.remote) {
            return undefined;
          }
          return {fqn: fqn};
        } else {
          return val;
        }
      },

      watch: function (obj, label, fqn) {
        var val = obj[label];

        function getter() {
          return val;
        }
        function setter(newVal) {
          if (val !== newVal && newVal !== fqnMap.get(fqn)) {
            // trigger some sort of change.
            if (val && typeof val === 'object') {
              fqnMap.deleteVar(fqn);
              socket.emit('del', [fqn]);
              val = newVal;
              lib.processScope(obj, fqn.substring(0, fqn.lastIndexOf('.')));
              return newVal;
            }
            if (newVal && typeof newVal === 'object') {
              fqnMap.deleteVar(fqn);
              socket.emit('del', [fqn]);
              val = newVal;
              lib.processScope(obj, fqn.substring(0, fqn.lastIndexOf('.')));
              return newVal;
            }
            fqnMap.set(fqn, newVal);
            val = newVal;
            if (typeof newVal === 'function') {
              newVal = {fqn: fqn};
            }
            var toReplace = {};
            toReplace[fqn] = newVal;
            socket.emit('rv', toReplace);
          }
          return newVal;
        }

        if (Object.defineProperty) {
          Object.defineProperty(obj, label, {get: getter, set: setter});
        } else {
          if (obj.__defineSetter__) {
            obj.__defineSetter__(label, setter);
          }
          if (obj.__defineGetter__) {
            obj.__defineGetter__(label, getter);
          }
        }
      },

      unwatch: function (obj, label) {
        //try {
        if (Object.defineProperty) {
          Object.defineProperty(obj, label, {get: undefined, set: undefined});
        } else {
          if (obj.__defineSetter__) {
            obj.__defineSetter__(label, undefined);
          }
          if (obj.__defineGetter__) {
            obj.__defineGetter__(label, undefined);
          }
        }
        //} catch (e) {}
      }
    };

    var now = {
      ready: function (func) {
        if (arguments.length === 0) {
          util.emit('ready');
        } else {
          if (nowReady) {
            func();
          }
          util.on('ready', func);
        }
      },
      core: {
        on: util.on,
        options: options,
        removeEvent: util.removeEvent,
        clientId: undefined,
        noConflict: noConflict
      }
    };

    lib = {
      deleteVar: function (fqn) {
        var path, currVar, parent, key;
        path = fqn.split('.');
        currVar = now;
        for (var i = 1; i < path.length; i++) {
          key = path[i];
          if (currVar === undefined) {
            // delete from fqnMap, just to be safe.
            fqnMap.deleteVar(fqn);
            return;
          }
          if (i === path.length - 1) {
            delete currVar[path.pop()];
            fqnMap.deleteVar(fqn);
            return;
          }
          currVar = currVar[key];
        }
      },

      replaceVar: function (data) {
        for (var fqn in data) {
          if (util.hasProperty(data[fqn], 'fqn')) {
            data[fqn] = lib.constructRemoteFunction(fqn);
          }
          util.createVarAtFqn(now, fqn, data[fqn]);
        }
      },

      remoteCall: function (data) {
        var func;
        // Retrieve the function, either from closures hash or from the now scope
        if (data.fqn.split('_')[0] === 'closure') {
          func = closures[data.fqn];
        } else {
          func = util.getVarFromFqn(now, data.fqn);
        }
        var i, ii, args = data.args;

        if (typeof args === 'object' && !util.isArray(args)) {
          var newargs = [];
          // Enumeration order is not defined so this might be useless,
          // but there will be cases when it works
          for (i in args) {
            newargs.push(args[i]);
          }
          args = newargs;
        }

        // Search (only at top level) of args for functions parameters,
        // and replace with wrapper remote call function
        for (i = 0, ii = args.length; i < ii; i++) {
          if (util.hasProperty(args[i], 'fqn')) {
            args[i] = lib.constructRemoteFunction(args[i].fqn);
          }
        }
        func.apply({now: now}, args);
      },

      // Handle the ready message from the server
      serverReady: function () {
        nowReady = true;
        lib.processNowScope();
        util.emit('ready');
      },

      constructRemoteFunction: function (fqn) {
        var remoteFn = function () {

          lib.processNowScope();

          var args = [];
          for (var i = 0, ii = arguments.length; i < ii; i++) {
            args[i] = arguments[i];
          }
          for (i = 0, ii = args.length; i < ii; i++) {
            if (typeof args[i] === 'function') {
              var closureId = 'closure_' + args[i].name + '_' + util.generateRandomString();
              closures[closureId] = args[i];
              args[i] = {fqn: closureId};
            }
          }
          socket.emit('rfc', {fqn: fqn, args: args});
        };
        remoteFn.remote = true;
        return remoteFn;
      },
      handleNewConnection: function (socket) {
        if (socket.handled) {
          return;
        }
        socket.handled = true;

        socket.on('rfc', function (data) {
          lib.remoteCall(data);
          util.emit('rfc', data);
        });
        socket.on('rv', function (data) {
          lib.replaceVar(data);
          util.emit('rv', data);
        });
        socket.on('del', function (data) {
          lib.deleteVar(data);
          util.emit('del', data);
        });

        // Handle the ready message from the server
        socket.on('rd', function (data) {
          if (++readied === 2) {
            lib.serverReady();
          }
        });

        socket.on('disconnect', function () {
          readied = 0;
          util.emit('disconnect');
        });
        // Forward planning for socket io 0.7
        socket.on('error', function () {
          util.emit('error');
        });
        socket.on('retry', function () {
          util.emit('retry');
        });
        socket.on('reconnect', function () {
          util.emit('reconnect');
        });
        socket.on('reconnect_failed', function () {
          util.emit('reconnect_failed');
        });
        socket.on('connect_failed', function () {
          util.emit('connect_failed');
        });
      },
      processNowScope: function () {
        lib.processScope(now, 'now');
        if (typeof lastTimeout === 'number') {
        	clearTimeout(lastTimeout);
        }        
        if (socket.socket.connected) {
          lastTimeout = setTimeout(lib.processNowScope, 1000);
        }
      },
      processScope: function (obj, path) {
        var data = {};
        lib.traverseScope(obj, path, data);
        // Send only for non-empty object
        for (var i in data) {
          if (util.hasProperty(data, i) && data[i] !== undefined) {
            socket.emit('rv', data);
            break;
          }
        }
      },
      traverseScope: function (obj, path, data) {

        if (obj && typeof obj === 'object') {
          var objIsArray = util.isArray(obj);
          var keys = fqnMap.get(path);
          for (var key in obj) {
            var fqn = path + '.' + key;

            if (fqn === 'now.core' || fqn === 'now.ready') {
              continue;
            }

            if (util.hasProperty(obj, key)) {

              var val = obj[key];
              var mapVal = fqnMap.get(fqn);
              var wasArray = fqnMap.arrays[fqn];
              var valIsArray = util.isArray(val);
              var valIsObj = val && typeof val === 'object';
              var wasObject = util.isArray(mapVal) && !wasArray;

              if (objIsArray || isIE) {
                if (valIsObj) {
                  if (valIsArray) {
                    // Value is an array
                    if (!wasArray) {
                      fqnMap.set(fqn, []);
                      fqnMap.flagAsArray(fqn);
                      data[fqn] = [];
                    }
                  } else {
                    // Value is object
                    if (!wasObject) {
                      fqnMap.set(fqn, []);
                      fqnMap.unflagAsArray(fqn);
                      data[fqn] = {};
                    }
                  }
                } else {
                  // Value is primitive / func
                  if (val !== mapVal) {
                    fqnMap.set(fqn, val);
                    fqnMap.unflagAsArray(fqn);
                    data[fqn] = util.getValOrFqn(val, fqn);
                  }
                }
              } else if (mapVal === undefined) {
                util.watch(obj, key, fqn);

                if (valIsObj) {
                  if (valIsArray) {
                    // Value is array
                    fqnMap.set(fqn, []);
                    fqnMap.flagAsArray(fqn);
                    data[fqn] = [];
                  } else {
                    // Value is object
                    fqnMap.set(fqn, []);
                    data[fqn] = {};
                  }
                } else {
                  // Value is primitive / func
                  fqnMap.set(fqn, val);
                  data[fqn] = util.getValOrFqn(val, fqn);
                }
              }
              if (valIsObj) {
                lib.traverseScope(val, fqn, data);
              }
            }
          }

          if (keys && typeof keys === 'object') {
            var toDelete = [];
            // Scan for deleted keys.
            for (var i = 0; i < keys.length; i++) {
              if (keys[i] !== undefined && obj[keys[i]] === undefined) {
                toDelete.push(path + '.' + keys[i]);
                fqnMap.deleteVar(path + '.' + keys[i]);
                --i;
              }
            }
            // Send message to server to delete from its database.
            if (toDelete.length > 0) {
              socket.emit('del', toDelete);
            }
          }
        }

      },

      traverseScopeIE: function (obj, path, data) {
      }


    };

    (function () {
      socket = io.connect(uri + '/', now.core.options.socketio || {});
      now.core.socketio = socket;
      socket.on('connect', function () {
        now.core.clientId = socket.socket.sessionid;
        lib.handleNewConnection(socket);
        // Begin intermittent scope traversal

        setTimeout(function () {
          lib.processNowScope();
          socket.emit('rd');
          if (++readied === 2) {
            nowReady = true;
            util.emit('ready');
          }
        }, 100);

        util.emit('connect');
      });
      socket.on('disconnect', function () {
        // y-combinator trick
        (function (y) {
          y(y, now);
        }(function (fn, obj) {
          for (var i in obj) {
            if (obj[i] && typeof obj[i] === 'object' && obj[i] !== now.core) {
              fn(fn, obj[i]);
            }
            else if (typeof obj[i] === 'function' && obj[i].remote) {
              delete obj[i];
            }
          }
        }));
        // Clear all sorts of stuff in preparation for reconnecting.
        fqnMap.data = {};
      });
    })();

    return (nowObjects[uri] = now);
  };
  exports.nowInitialize = noConflict;
}());